first pass at a weibo publishing agent, almost identical to the twitter one

updating weibo agent with app_key/app_secret options to make it work. and some custom code for if the input event comes from twitter to unwrap t.co urls as those are not allowed on weibo

better spec for weibo agent

text edits

changing user_id to uid

Albert Sun 11 ans auparavant
Parent
Commettre
8754e50a02

+ 1 - 0
Gemfile

@@ -37,6 +37,7 @@ gem 'wunderground'
37 37
 gem "twitter"
38 38
 gem 'twitter-stream', '>=0.1.16'
39 39
 gem 'em-http-request'
40
+gem 'weibo_2'
40 41
 
41 42
 platforms :ruby_18 do
42 43
   gem 'system_timer'

+ 17 - 0
Gemfile.lock

@@ -100,11 +100,13 @@ GEM
100 100
       rails (~> 3.0)
101 101
     haml (4.0.2)
102 102
       tilt
103
+    hashie (2.0.5)
103 104
     hike (1.2.1)
104 105
     http_parser.rb (0.5.3)
105 106
     httparty (0.10.2)
106 107
       multi_json (~> 1.0)
107 108
       multi_xml (>= 0.5.2)
109
+    httpauth (0.2.0)
108 110
     i18n (0.6.1)
109 111
     journey (1.0.4)
110 112
     jquery-rails (2.2.1)
@@ -137,6 +139,13 @@ GEM
137 139
     mysql2 (0.3.11)
138 140
     nested_form (0.3.2)
139 141
     nokogiri (1.5.9)
142
+    oauth2 (0.9.1)
143
+      faraday (~> 0.8)
144
+      httpauth (~> 0.1)
145
+      jwt (~> 0.1.4)
146
+      multi_json (~> 1.0)
147
+      multi_xml (~> 0.5)
148
+      rack (~> 1.2)
140 149
     orm_adapter (0.4.0)
141 150
     polyglot (0.3.3)
142 151
     pry (0.9.12)
@@ -187,6 +196,8 @@ GEM
187 196
     rdoc (3.12.2)
188 197
       json (~> 1.4)
189 198
     remotipart (1.0.5)
199
+    rest-client (1.6.7)
200
+      mime-types (>= 1.16)
190 201
     rr (1.0.4)
191 202
     rspec (2.13.0)
192 203
       rspec-core (~> 2.13.0)
@@ -253,6 +264,11 @@ GEM
253 264
     webmock (1.11.0)
254 265
       addressable (>= 2.2.7)
255 266
       crack (>= 0.3.2)
267
+    weibo_2 (0.1.4)
268
+      hashie (~> 2.0.4)
269
+      multi_json (~> 1.7.2)
270
+      oauth2 (~> 0.9.1)
271
+      rest-client (~> 1.6.7)
256 272
     wunderground (1.0.0)
257 273
       addressable
258 274
       httparty (> 0.6.0)
@@ -298,4 +314,5 @@ DEPENDENCIES
298 314
   typhoeus
299 315
   uglifier (>= 1.0.3)
300 316
   webmock
317
+  weibo_2
301 318
   wunderground

+ 91 - 0
app/models/agents/weibo_publish_agent.rb

@@ -0,0 +1,91 @@
1
+require "weibo_2"
2
+
3
+module Agents
4
+  class WeiboPublishAgent < Agent
5
+    cannot_be_scheduled!
6
+
7
+    description <<-MD
8
+      The WeiboPublishAgent publishes tweets from the events it receives.
9
+
10
+      You must first set up a Weibo app and generate an `acess_token` for the user to send statuses as.
11
+
12
+      Include that in options, along with the `app_key` and `app_secret` for your Weibo app. It's useful to also include the Weibo user id of the person to publish as.
13
+
14
+      You must also specify a `message_path` parameter: a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value to tweet.
15
+
16
+      Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
17
+    MD
18
+
19
+    def validate_options
20
+      unless options[:uid].present? &&
21
+        options[:expected_update_period_in_days].present? &&
22
+        options[:app_key].present? &&
23
+        options[:app_secret].present? &&
24
+        options[:access_token].present?
25
+        errors.add(:base, "expected_update_period_in_days, uid, and access_token are required")
26
+      end
27
+    end
28
+
29
+    def working?
30
+      (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? && event.payload[:success] == true
31
+    end
32
+
33
+    def default_options
34
+      {
35
+          :uid => "",
36
+          :access_token => "---",
37
+          :app_key => "---",
38
+          :app_secret => "---",
39
+          :expected_update_period_in_days => "10",
40
+          :message_path => "text"
41
+      }
42
+    end
43
+
44
+    def receive(incoming_events)
45
+      # if there are too many, dump a bunch to avoid getting rate limited
46
+      if incoming_events.count > 20
47
+        incoming_events = incoming_events.first(20)
48
+      end
49
+      incoming_events.each do |event|
50
+        tweet_text = Utils.value_at(event.payload, options[:message_path])
51
+        if event.agent.type == "Agents::TwitterUserAgent"
52
+          tweet_text = unwrap_tco_urls(tweet_text, event.payload)
53
+        end
54
+        begin
55
+          publish_tweet tweet_text
56
+          create_event :payload => {
57
+            :success => true,
58
+            :published_tweet => tweet_text,
59
+            :agent_id => event.agent_id,
60
+            :event_id => event.id
61
+          }
62
+        rescue OAuth2::Error => e
63
+          create_event :payload => {
64
+            :success => false,
65
+            :error => e.message,
66
+            :failed_tweet => tweet_text,
67
+            :agent_id => event.agent_id,
68
+            :event_id => event.id
69
+          }
70
+        end
71
+      end
72
+    end
73
+
74
+    def publish_tweet text
75
+      WeiboOAuth2::Config.api_key = options[:app_key] # WEIBO_APP_KEY
76
+      WeiboOAuth2::Config.api_secret = options[:app_secret] # WEIBO_APP_SECRET
77
+      client = WeiboOAuth2::Client.new
78
+      client.get_token_from_hash :access_token => options[:access_token]
79
+
80
+      client.statuses.update text
81
+    end
82
+
83
+    def unwrap_tco_urls text, tweet_json
84
+      tweet_json[:entities][:urls].each do |url|
85
+        text.gsub! url[:url], url[:expanded_url]
86
+      end
87
+      return text
88
+    end
89
+
90
+  end
91
+end

+ 128 - 0
spec/data_fixtures/one_tweet.json

@@ -0,0 +1,128 @@
1
+{ 
2
+    "created_at": "Sat Jun 15 20:10:32 +0000 2013", 
3
+    "id": 345996769290752000, 
4
+    "id_str": "345996769290752000", 
5
+    "text": "Crytoscape is a graph manipulation library for JS.  Impressive.  http://t.co/KQFGZWvkSs", 
6
+    "source": "<a href=\"http://tapbots.com/software/tweetbot/mac\" rel=\"nofollow\">Tweetbot for Mac</a>", 
7
+    "truncated": false, 
8
+    "in_reply_to_status_id": null, 
9
+    "in_reply_to_status_id_str": null, 
10
+    "in_reply_to_user_id": null, 
11
+    "in_reply_to_user_id_str": null, 
12
+    "in_reply_to_screen_name": null, 
13
+    "user": { 
14
+        "id": 9813372, 
15
+        "id_str": "9813372", 
16
+        "name": "Andrew Cantino", 
17
+        "screen_name": "tectonic", 
18
+        "location": "San Francisco, CA", 
19
+        "description": "Experimentalist, web developer, and VP of Engineering at @Mavenlink.", 
20
+        "url": "http://t.co/SKoQz7cOVI", 
21
+        "entities": { 
22
+            "url": { 
23
+                "urls": [ 
24
+                    { 
25
+                        "url": "http://t.co/SKoQz7cOVI", 
26
+                        "expanded_url": "http://andrewcantino.com", 
27
+                        "display_url": "andrewcantino.com", 
28
+                        "indices": [ 
29
+                            0, 
30
+                            22 
31
+                        ] 
32
+                    } 
33
+                ] 
34
+            }, 
35
+            "description": { 
36
+                "urls": [] 
37
+            } 
38
+        }, 
39
+        "protected": false, 
40
+        "followers_count": 1056, 
41
+        "friends_count": 492, 
42
+        "listed_count": 37, 
43
+        "created_at": "Wed Oct 31 03:16:39 +0000 2007", 
44
+        "favourites_count": 151, 
45
+        "utc_offset": -28800, 
46
+        "time_zone": "Pacific Time (US & Canada)", 
47
+        "geo_enabled": true, 
48
+        "verified": false, 
49
+        "statuses_count": 3628, 
50
+        "lang": "en", 
51
+        "contributors_enabled": false, 
52
+        "is_translator": false, 
53
+        "profile_background_color": "352726", 
54
+        "profile_background_image_url": "http://a0.twimg.com/images/themes/theme5/bg.gif", 
55
+        "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme5/bg.gif", 
56
+        "profile_background_tile": false, 
57
+        "profile_image_url": "http://a0.twimg.com/profile_images/1694984565/me-right_normal.jpg", 
58
+        "profile_image_url_https": "https://si0.twimg.com/profile_images/1694984565/me-right_normal.jpg", 
59
+        "profile_link_color": "D02B55", 
60
+        "profile_sidebar_border_color": "829D5E", 
61
+        "profile_sidebar_fill_color": "99CC33", 
62
+        "profile_text_color": "3E4415", 
63
+        "profile_use_background_image": true, 
64
+        "default_profile": false, 
65
+        "default_profile_image": false, 
66
+        "following": true, 
67
+        "follow_request_sent": false, 
68
+        "notifications": null 
69
+    }, 
70
+    "geo": null, 
71
+    "coordinates": null, 
72
+    "place": { 
73
+        "id": "866269c983527d5a", 
74
+        "url": "https://api.twitter.com/1.1/geo/id/866269c983527d5a.json", 
75
+        "place_type": "neighborhood", 
76
+        "name": "Ashbury Heights", 
77
+        "full_name": "Ashbury Heights, San Francisco", 
78
+        "country_code": "US", 
79
+        "country": "United States", 
80
+        "bounding_box": { 
81
+            "type": "Polygon", 
82
+            "coordinates": [ 
83
+                [ 
84
+                    [ 
85
+                        -122.45778216, 
86
+                        37.75932999 
87
+                    ], 
88
+                    [ 
89
+                        -122.44248216, 
90
+                        37.75932999 
91
+                    ], 
92
+                    [ 
93
+                        -122.44248216, 
94
+                        37.767528989999995 
95
+                    ], 
96
+                    [ 
97
+                        -122.45778216, 
98
+                        37.767528989999995 
99
+                    ] 
100
+                ] 
101
+            ] 
102
+        }, 
103
+        "attributes": {} 
104
+    }, 
105
+    "contributors": null, 
106
+    "retweet_count": 0, 
107
+    "favorite_count": 2, 
108
+    "entities": { 
109
+        "hashtags": [], 
110
+        "symbols": [], 
111
+        "urls": [ 
112
+            { 
113
+                "url": "http://t.co/KQFGZWvkSs", 
114
+                "expanded_url": "http://cytoscape.github.io/cytoscape.js/", 
115
+                "display_url": "cytoscape.github.io/cytoscape.js/", 
116
+                "indices": [ 
117
+                    65, 
118
+                    87 
119
+                ] 
120
+            } 
121
+        ], 
122
+        "user_mentions": [] 
123
+    }, 
124
+    "favorited": false, 
125
+    "retweeted": false, 
126
+    "possibly_sensitive": false, 
127
+    "lang": "en" 
128
+}

+ 13 - 0
spec/fixtures/agents.yml

@@ -79,3 +79,16 @@ bob_rain_notifier_agent:
79 79
                   }],
80 80
                  :message => "Just so you know, it looks like '<conditions>' tomorrow in <zipcode>"
81 81
                }.to_yaml.inspect %>
82
+
83
+bob_twitter_user_agent:
84
+  type: Agents::TwitterUserAgent
85
+  user: bob
86
+  name: "Bob's Twitter User Watcher"
87
+  options: <%= {
88
+      :username => "tectonic",
89
+      :expected_update_period_in_days => "2",
90
+      :consumer_key => "---",
91
+      :consumer_secret => "---",
92
+      :oauth_token => "---",
93
+      :oauth_token_secret => "---"
94
+    }.to_yaml.inspect %>

+ 69 - 0
spec/models/agents/weibo_publish_agent_spec.rb

@@ -0,0 +1,69 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::WeiboPublishAgent do
4
+  before do
5
+    @opts = {
6
+      :uid => "1234567",
7
+      :expected_update_period_in_days => "2",
8
+      :app_key => "---",
9
+      :app_secret => "---",
10
+      :access_token => "---",
11
+      :message_path => "text"
12
+    }
13
+
14
+    @checker = Agents::WeiboPublishAgent.new(:name => "Weibo Publisher", :options => @opts)
15
+    @checker.user = users(:bob)
16
+    @checker.save!
17
+
18
+    @event = Event.new
19
+    @event.agent = agents(:bob_weather_agent)
20
+    @event.payload = { :text => 'Gonna rain..' }
21
+    @event.save!
22
+
23
+    @sent_messages = []
24
+    stub.any_instance_of(Agents::WeiboPublishAgent).publish_tweet { |message| @sent_messages << message}
25
+  end
26
+
27
+  describe '#receive' do
28
+    it 'should publish any payload it receives' do
29
+      event1 = Event.new
30
+      event1.agent = agents(:bob_rain_notifier_agent)
31
+      event1.payload = { :text => 'Gonna rain..' }
32
+      event1.save!
33
+
34
+      event2 = Event.new
35
+      event2.agent = agents(:bob_weather_agent)
36
+      event2.payload = { :text => 'More payload' }
37
+      event2.save!
38
+
39
+      Agents::WeiboPublishAgent.async_receive(@checker.id, [event1.id, event2.id])
40
+      @sent_messages.count.should eq(2)
41
+      @checker.events.count.should eq(2)
42
+    end
43
+  end
44
+
45
+  describe '#receive a tweet' do
46
+    it 'should publish a tweet after expanding any t.co urls' do
47
+      event = Event.new
48
+      event.agent = agents(:bob_twitter_user_agent)
49
+      event.payload = JSON.parse(File.read(Rails.root.join("spec/data_fixtures/one_tweet.json")))
50
+      event.save!
51
+
52
+      Agents::WeiboPublishAgent.async_receive(@checker.id, [event.id])
53
+      @sent_messages.count.should eq(1)
54
+      @checker.events.count.should eq(1)
55
+      @sent_messages.first.include?("t.co").should_not be_true
56
+    end
57
+  end
58
+
59
+  describe '#working?' do
60
+    it 'checks if events have been received within the expected receive period' do
61
+      @checker.should_not be_working # No events received
62
+      Agents::WeiboPublishAgent.async_receive(@checker.id, [@event.id])
63
+      @checker.reload.should be_working # Just received events
64
+      two_days_from_now = 2.days.from_now
65
+      stub(Time).now { two_days_from_now }
66
+      @checker.reload.should_not be_working # More time has passed than the expected receive period without any new events
67
+    end
68
+  end
69
+end